準備工作都完成了,接下來就可以開始建立第一個神奇的tauri程式了
好容易把套件裝起來了,先依官網指示建立一個demo app 吧
因為我們的專案之後會有很多支程式放一起,所以前端和後端就 全部摻在一起做成撒尿牛丸就好了 要先建好基本的分層結構,以下建立我們的code base,在這邊取名為demo-app,後續這個名字代表的就是整個code base。
~$ mkdir demo-app    # 建立 demo-app資料夾
~$ cd demo-app/      # 進入 demo-app資料夾
~/demo-app$ cargo install create-tauri-app --locked    # 安裝tauri 範本工具cli
   Updating crates.io index
  Downloaded create-tauri-app v3.7.3
  Downloaded 1 crate (263.2 KB) in 7.08s
  Installing create-tauri-app v3.7.3
... 略 ...
   Compiling create-tauri-app v3.7.3
    Finished release [optimized] target(s) in 24.52s
  Installing /home/heyman/.cargo/bin/cargo-create-tauri-app
   Installed package `create-tauri-app v3.7.3` (executable `cargo-create-tauri-app`)
接著我們在資料夾中建立前端專案,透過create-tauri-app工具建立tauri 專案,執行後會出現提示訊息,依下列步驟逐項選擇即可,當然你很熟的話也可以挑戰選擇其他的選項。
~/demo-app$ cargo create-tauri-app app     # 建立 tauri app
選TypeScript,前端要用rust也可以,但是感覺 很硬 就離題了,所以還是先選大家比較常見的前端語言 TypeScript
pnpm 就是快,你還沒用嗎,趕快去了解吧。
這裡選svelte
TypeScript
跑完結果如下:
現在的CLI工具都會很貼心的告訴你下一步要打的指令是什麼,不過繼續之前我們先看一下所產生的資料夾結構長什麼樣子:
~/demo-app$ tree app
app
├── index.html            # SPA 單網頁的那頁html
├── package.json          # node.js 都會看到的專案設定檔
├── public (略)           # 打包web用靜態檔案
├── README.md
├── src                   # node.js 專案的src目錄
│   ├── App.svelte        # Svetle 的首頁
│   ├── lib               # Svelte 的 component 資料夾
│   │   └── Greet.svelte  # Svelte 的 component 檔案
│   ├── main.ts           # web 程式進入點
│   ├── styles.css        # 樣式表
│   └── vite-env.d.ts
├── src-tauri             # Tauri 程式的目錄
│   ├── build.rs          # rust 建構腳本
│   ├── Cargo.toml        # Rust 專案設定檔
│   ├── icons (略)
│   ├── src
│   │   └── main.rs       # Rust 程式的進入點
│   └── tauri.conf.json   # tauri 啟動設定檔
├── svelte.config.js      # Svelte 設定檔
├── tsconfig.json         # TypeScript 編譯設定檔 
├── tsconfig.node.json
└── vite.config.ts        # Vite 設定檔
用node.js開發過前端的捧友們應該覺得很熟悉,除了多出來的src-tauri資料夾。沒錯,整個專案如果忽略tauri的資料夾,直接用nodejs也可以跑的很開心。
接著我們不免俗地先執行pnpm還原相依套件,最後再執行我們的程式
~/demo-app$ cd app                 # 進入剛剛產生的目錄
~/demo-app/app$ pnpm i             # 安裝 svelte 前端套件
~/demo-app/app$ cargo tauri dev    # 開發 tauri app
如果有人還是喜歡用 yarn 或 npm的話,自己調整指令即可
rust的編譯時間比較久,等待的同時,我們來看一下package.json檔放了什麼內容:
{   
    // 略
    "scripts": {
        "dev": "vite",
        "build": "vite build",
        "preview": "vite preview",
        "check": "svelte-check --tsconfig ./tsconfig.json",
        "tauri": "tauri"
  }
}
所以除了tauri那行是新的,其他看起來就是在開發前端,是的,我們再接著往下看。
等執行完跳出下面的畫面就表示成功了,下面的三個logo圖示分別為Vite、Tauri及Svelte,三個願望一次滿足 (?):
終於把UI弄出來了,有操作畫面還是比較直觀一點,我們立馬試一下互動,輸入名字後按 Greet看看:
看起來好像沒什麼(?),不過等等,開發過前端框架的眼尖朋友們,是不是有注意到,在剛剛打 cargo 開發指令時是不是好像有出現一個熟悉的畫面,這是dev server跑起來的感覺啊:
那我們試著用Chrome開啟上面的連結http://localhost:1420/看看會怎樣
溫馨小提示:按住Ctrl鍵再用滑鼠點命令模式中的網址就可以直接跳轉了
竟然也可以開耶,那按一下Greet會怎樣?
竟然沒反應!趕快看一下F12(Crhome or Edge)是怎麼回事:
- Safari 要先開啟開發人員模式,才能按Option + ⌘ + C。
- Firefox 按 Ctrl + Shift + I
什麼是 __TAURI_IPC__ is not a function,我們來看一下程式碼app/src/lib/Greet.svelte:
<!-- app/src/lib/Greet.svelte -->
<script lang="ts">
  import { invoke } from "@tauri-apps/api/tauri"
  let name = "";
  let greetMsg = ""
  async function greet(){
    greetMsg = await invoke("greet", { name }) // <= 這裡有個奇怪的function 
  }
</script>
<div>
  <form class="row" on:submit|preventDefault={greet}>
    <input id="greet-input" placeholder="Enter a name..." bind:value={name} />
    <button type="submit">Greet</button>
  </form>
  <p>{greetMsg}</p>
</div>
原來Greet的按鈕是在第8行呼叫 invoke 方法,該方法是從 tauri api 引入而來的,那我們再到看一下tauri裡的rust程式碼 app/src-tauri/src/main.rs
// app/src-tauri/src/main.rs
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
聰明的你應該注意到了,第2行有一個fn greet,沒錯,它就是rust的function(後面可能會依情境稱為函數、函式、方法或fn),原來剛剛在前端呼叫的invoke("greet", { name })是呼叫 rust 裡定義的fn,並傳遞JSON參數name給rust同名參數name使用。
如果你現在的心情像下圖一下,先暫停一下,再回看兩次,應該就懂了
如果有不知道
{ name }☰{ name: name }的同學去看一下object-shorthand,這個shorthand不是showhand唷,是港片看太多逆(?)
溫馨提醒:這個物件shorthand的語法在rust中也可以使用唷 ^.<
原來是直接呼叫tauri裡的rust代碼,所以我們剛剛直接在瀏覽器裡點Greet,當然就呼叫不到tauri了,不過如果我們的JavaScript直接呼叫後端api的話,是不是表示在這裡寫的前端也可以獨立運作了呢。心動不如馬上行動,我們來實驗一下:
先在網路找一個現成的example api,依它提供的規格,我們在Greet.svelte裡加上以下片段:
<script>
    // 略
  let data = [];
  const callMyApi = async () => {
    const response = await fetch("https://dummy.restapiexample.com/api/v1/employees");
    const body = await response.json();
    data = body.data;
    console.log(data);    // 我剛剛先偷看資料的長相,才把下面each裡的程式碼調整對
  }
</script>
<div>
  <!-- 略 -->
  <button on:click={callMyApi}>Call my API</button>
  <ul>
    {#each data as item}
      <li>{item.employee_name}</li>
    {/each}
  </ul>
</div>
修改後按存檔,畫面會自動hot reload,這時候畫面會新增一個 Call my API的按鈕,我們按下去看看:
果真跟我們剛剛預期的一樣,不過,實際佈署的話呢?我們還是試一下,古時候沒有完善的DevOps工具時,常常發生的可怕事件之一就是:我開發跑明明都沒問題啊,怎麼佈版上去就死了,懂的就懂(?)。
我們利用剛剛 package.json 裡設好的 build 指令建置:
~/demo-app/app$ pnpm build   # 前端框架的打包工具
vite打包完成的檔案不出意外的放在 dist 資料夾下,我們把它丟到http server跑起來看看,但想到要分別安裝linux, mac, windows的http server就累了,考量有些windows的朋友可能不想裝IIS (先承認你朋友就是你) ,我們用更簡單的方式來起http server,感謝現在node豐富的生態系,我們直接找看看npm裡有沒有現成的http static server,很幸運地,第一個看起來就是我們要的:
通常在搜尋完先不要急著點第一個,建議再多看幾個,比較一下活躍度、星星數、最後更新、以及簡單說明之類的,再去嘗試。
比較後看起來第一個似乎比較新,就用它試試看好了。進去看一下http-server的說明,照著安裝:
~/demo-app/app$ pnpm install --global http-server  # 安裝 http-server的npm至電腦全域環境
Packages: +40
++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: /home/yesido/.local/share/pnpm/store/v3
  Virtual store is at:             .pnpm
Progress: resolved 40, reused 20, downloaded 20, added 40, done
/home/yesido/.local/share/pnpm/global/5:
+ http-server 14.1.1
Done in 2.9s
再照著http-server的說明跑起來:
~/demo-app/app$ http-server dist/  # 把我們剛剛pnpm build的結果資料夾 dist作為靜態網頁資料夾
Starting up http-server, serving dist/
http-server version: 14.1.1
http-server settings: 
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none
Available on:
  http://127.0.0.1:8080
  http://192.168.0.25:8080
Hit CTRL-C to stop the server
看起來沒問題,我們用Browser開看看:
按Get my API 功能取得資料並顯示,與剛剛開發時測試的功能一樣。所以表示剛剛我們想刻一套前端,同時讓web 瀏覽器用也可以同時讓桌面應用程式使用的想法是可行的(理論上啦,實際看我們後面會踩到什麼雷(?)。
我們打算下一篇來實作rust code,在此之前還有一點時間,我們先做一下苦工(暖個身?),一般在專案中,會採分層方式來管理程式碼,避免變成大泥球(?),你終究要變大泥球的,那為什麼不一開始就變,如果還沒聽過分層的可以收藏一下這個視頻,有時間一定要去好好聽一下。
維護workspace的步驟我不會用指令(>.<)。所以檔案都是手動改的,如果有好心的大大知道怎麼做比較快,歡迎補充一下
如何設定 rust 的工作空間(workspace),首先依官方說明,我們在一開始的 demo-app 底下新增一個檔案Cargo.toml,內容如下:
[workspace]
members = [
    "app/src-tauri",
]
記得去加上.gitignore檔。
我們再開幾個專案分層試一下,順便看看rust怎麼組織不同的專案檔:
~/demo-app/app$ cd ..                # 回上一層 (如果你在子專案裡面目錄)
~/demo-app$ cargo new core --lib     # 新增core lib 專案
~/demo-app$ cargo new service --lib  # 新增service lib 專案
~/demo-app$ cargo new web            # 新增web bin 專案
以上指令我們新增一個名稱 core 的專案,拿來放核心的業務邏輯(或有人叫商業邏輯Business Logic),或領域核心邏輯層Domain Layer,再加一個service 應用層的專案,由於這兩個都是Library(程式庫),僅供呼叫使用,不是執行檔(bin),所以加--lib參數,以下我們看一下檔案結構的比較。
~/demo-app$ tree core/ service/ web/
core/
├── Cargo.toml
└── src
    └── lib.rs
service/
├── Cargo.toml
└── src
    └── lib.rs
web/
├── Cargo.toml
└── src
    └── main.rs
可以看到lib專案只有lib.rs,bin專案有一個main.rs,剛才細心看tauri說明的同學,就知道剛剛有說main.rs是rust程式的進入點。可想而知,web專案可以單獨執行,core及service只能提供讓專案使用呼叫。這裡可以把這幾個專案的單位稱為crate。
剛剛執行的時候應該有一些提示訊息,我們回過頭來看一下它說什麼,它提到workspace沒有我們剛剛加入的專案。
this may be fixable by adding `core` to the `workspace.members` array of the manifest located at: /home/whoami/demo-app/Cargo.toml
Alternatively, to keep it out of the workspace, add the package to the `workspace.exclude` array, or add an empty `[workspace]` table to the package's manifest.
所以我們還要手動去編輯剛剛維護workspace的 Cargo.toml,手動加入以下專案成員如下:
[workspace]
members = [
    "app/src-tauri",
    "core",
    "service",
    "web",
]
註:這裡的member是相對的資料夾路徑,Cargo會再去該資料夾裡找該子專案的Cargo.toml檔,依該檔案的個別設定執行該專案。
加完後我們可以試run看看:
~/demo-app$ cargo run -p core
error: a bin target must be available for `cargo run`
~/demo-app$ cargo run -p service
error: a bin target must be available for `cargo run`
~/demo-app$ cargo run -p web
   Compiling web v0.1.0 (/home/whoami/demo-app/web)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/web`
Hello, world!
上面的
-p參數是指定我們要執行哪一個 package,不是指目錄,只是這裡我用的名稱剛好一樣,這個專案名稱的參數寫在Cargo.toml檔案中。
接下來測試一下我們想要的依賴關係 web → service → core,是否可以正確的依循,修改以下檔案(僅列出部分內容):
// core/src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
    println!("add fn called in core");
    left + right              // 相當於 return left + right;
}
Rust的function最後一行的expression就是return值,最後一行不需寫return也不能加上;結尾,如果要提早回傳是可以使用關鍵字return的(跟ruby好像唷)。
# service/Cargo.toml
[dependencies]
core = { path = "../core" }
// service/src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
    println!("add fn called in service");
    core::add(left, right)        // 記得不能加分號結尾
}
# web/Cargo.toml
[dependencies]
core = { path = "../core" }
service = { path = "../service" }
// web/src/main.rs
fn main() {
    println!("Hello, world!");
    println!("call core fn from server: {}",core::add(2, 2));
    println!("call service fn from server: {}",service::add(2, 2));
}
以上修改完我們執行一下web程式看有沒有正確:
~/demo-app$ cargo run -p web
   Compiling core v0.1.0 (/home/whoami/demo-app/core)
   Compiling service v0.1.0 (/home/whoami/demo-app/service)
   Compiling web v0.1.0 (/home/whoami/demo-app/web)
    Finished dev [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/web`
Hello, world!
add fn called in core
call core fn from server: 4
add fn called in service
add fn called in core
call service fn from server: 4
It works!!,沒看到錯誤真是一件值得開心的事 XDD。好了,怕大家累了,先在此稍作歇息,下一回合我們再開始用rust來寫核心邏輯吧。
我在linux環境(debian+kde)一直跳錯,處理方式原則上就是看log訊息,訊息顯示缺什麼,就去安裝它,以下是手動安裝的一些項目供參考
sudo apt-get install libglib2.0-dev
sudo apt-get install libgtk-4-dev
sudo apt-get install librust-gdk-dev
sudo apt-get install libsoup2.4
sudo apt-get install libgssapi
sudo apt-get install krb5*
sudo apt-get install libgssapi-krb5-2
sudo apt-get install libgssapi-perl
sudo apt-get install gir1.2-javascriptcoregtk-4.0
sudo apt-get install libjavascriptcoregtk-4.0
sudo apt-get install libwebkit2gtk-4.0-dev
sudo apt-get install libglobus-gssapi-gsi-dev
sudo apt-get install libprotobuf-dev
sudo apt-get install protobuf-compiler
看起來是因為pnpm未設定全域環境的原因,照著提示的指令
pnpm setup
再依提示執行就可以了
source /home/you/.bashrc # 把you代成你的帳號
備註:本系列所使用的代碼部分由 副駕駛 共同完成。
程式原始碼同步放置於 https://github.com/kenstt/demo-app